﻿/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Text;

namespace QUT.Bio.Util {
	/// <summary>
	/// ObjectWrapper packages an object defined in the application space, such as a Gene or a BlastHit, as an indexed
	/// collection of (key,value) pairs.
	/// <list>
	/// <item>This permits viewers and editors to be automatically constructed and attached to application objects.</item>
	/// <item>TODO: does it make sense for us to require and implement INotifyPropertyChanged so that we can data bind onto this, and thence indirectly onto the underlying application object.</item>
	/// <item>TODO: Is the IEnumerable necessary? See how usage unfolds.</item>
	/// <item>TODO: Should I be using dependencyProperties? How would I do that? Codedom? Users of this library will looking for a system that automates the graph creation process as far as possible. Dependency properties are about as opposite to that as can be.</item>
	/// </list>
	/// </summary>
	public class ObjectWrapper
		: IEnumerable<KeyValuePair<string, object>> {

		/// <summary>
		/// The encapsulated application object.
		/// </summary>

		private object content;

		private PropertyIndexer properties;

		/// <summary>
		///	Initialise a new application object wrapper.
		/// </summary>
		/// <param name="content">The encapsulated application object.</param>

		public ObjectWrapper ( object content ) {
			Content = content;
		}

		/// <summary>
		/// Get or set the encapsulated object.
		/// </summary>

		public object Content {
			get {
				return content;
			}
			set {
				this.content = value;
				this.properties = new PropertyIndexer( content.GetType() );
			}
		}

		/// <summary>
		///	Provide an indexer to dynamically get and set properties.
		///	<para>If the input value is assignable to the destination property type, we perform a direct assignment.</para>
		///	<para>If the input value is a string, we attempt to find a static Parse method in the destination type, and use that method to convert the value.</para>
		/// </summary>
		/// <param name="propertyName">The name of the property to access or mutate.</param>
		/// <returns>An object reference to the named property value.</returns>
		/// <exception cref="System.Collections.Generic.KeyNotFoundException">KeyNotFoundException if the encapsulated object does not have a property with the supplied name.</exception>

		public object this[string propertyName] {
			get {
				PropertyInfo propertyInfo = content.GetType().GetProperty( propertyName );

				if ( propertyInfo == null ) {
					throw new KeyNotFoundException( "Property '" + propertyName + "' not found in encapsulated object." );
				}
				else {
					return propertyInfo.GetValue( content, null );
				}
			}
			set {
				PropertyInfo propertyInfo = content.GetType().GetProperty( propertyName );

				if ( propertyInfo == null ) {
					throw new KeyNotFoundException( "Property '" + propertyName + "' not found in encapsulated object." );
				}

				if ( !propertyInfo.CanWrite ) {
					throw new Exception( String.Format( "Attempt to set immutable property {0}", propertyName ) );
				}

				Type valueType = value.GetType();
				Type propertyType = propertyInfo.PropertyType;

				if ( propertyInfo.PropertyType.IsAssignableFrom( valueType ) ) {
					propertyInfo.SetValue( content, value, null );
				}
				else if ( valueType.Equals( typeof( string ) ) ) {
					MethodInfo parser = propertyType.GetMethod( "Parse", new Type[] { typeof( string ) } );
					propertyInfo.SetValue( content, parser.Invoke( null, new object[] { value } ), null );
				}
				else {
					// TODO: additional branch for client-supplied conversion function.
					throw new Exception( "Unable to convert object value '" + value + "' to type " + propertyType.Name );
				}

			}
		}

		/// <summary>
		/// Gets an enumerator over this object's properties.
		/// </summary>
		/// <returns>An enumerator over KeyValuePairs represetning the properties of this object.</returns>

		public IEnumerator<KeyValuePair<string, object>> GetEnumerator () {
			foreach ( PropertyInfo propertyInfo in content.GetType().GetProperties() ) {
				yield return new KeyValuePair<string, object>( propertyInfo.Name, propertyInfo.GetValue( content, null ) );
			}
		}

		/// <summary>
		/// Gets an enumerator over this object's properties.
		/// </summary>
		/// <returns>An enumerator over KeyValuePairs represetning the properties of this object.</returns>

		IEnumerator IEnumerable.GetEnumerator () {
			foreach ( PropertyInfo propertyInfo in content.GetType().GetProperties() ) {
				yield return new KeyValuePair<string, object>( propertyInfo.Name, propertyInfo.GetValue( content, null ) );
			}
		}

		/// <summary>
		/// Determines if the encapsulated object has a property with the specified name.
		/// </summary>
		/// <param name="propertyName">The name of the property to search for.</param>
		/// <returns></returns>

		public bool HasProperty ( string propertyName ) {
			return content.GetType().GetProperty( propertyName ) != null;
		}

		/// <summary>
		/// Gets a list containing the names of all properties that have been decorated with
		/// the DisplayedAttribute.
		/// </summary>
		/// <returns>A list containing the names of all displayed attributes in the content object.</returns>

		public PropertyInfo[] DisplayedProperties {
			get {
				List<PropertyInfo> displayedProperties = new List<PropertyInfo>();

				foreach ( PropertyInfo property in content.GetType().GetProperties() ) {
					if ( property.IsDefined( typeof( DisplayedAttribute ), true ) ) {
						displayedProperties.Add( property );
					}
				}

				return displayedProperties.ToArray();
			}
		}

		public ReadonlyIndexer<string, PropertyInfo> Properties {
			get {
				return properties;
			}
		}

		private class PropertyIndexer : ReadonlyIndexer<string, PropertyInfo> {
			private Type type;

			public PropertyIndexer ( Type type ) {
				this.type = type;
			}

			public PropertyInfo this[string i] {
				get {
					return type.GetProperty( i );
				}
			}

			public IEnumerator<PropertyInfo> GetEnumerator () {
				foreach ( PropertyInfo property in type.GetProperties() ) {
					yield return property;
				}
			}

			IEnumerator IEnumerable.GetEnumerator () {
				return type.GetProperties().GetEnumerator();
			}
		}
	}
}
